$this_path = Split-Path -Parent ($MyInvocation.MyCommand.Definition)
$this_user=$env:username
$this_machine=$env:computername
Function script:Popup-Message(){
    param(
        [Parameter(Mandatory=$True)]
        [string]$info,
        [Parameter(Mandatory=$False)]
        [string]$window_title='Script Running Error',
        [Parameter(Mandatory=$False)]
        [string]$info_type='Error'
    )
    [void][System.Windows.Forms.MessageBox]::Show($info, $window_title, 'OK', $info_type)
}
Function script:Start-BulkThreads(){	# -Action { Stop-Process -Id $PID }
	# useing parallel threads to run scripts in the same time so that it will speedup the running duration
	#reference https://www.codeproject.com/tips/895840/multi-threaded-powershell-cookbook
    param(
    	[Parameter(Mandatory=$True)]
    	[Object[]]$vdilist,             # the vdi list that scripts to be ran
    	[Parameter(Mandatory=$True)]    
    	[ScriptBlock]$Action,           # the actual action that each thread will run
    	[Parameter(Mandatory=$True)]    
    	[AllowNull()]
    	[Object[]]$ActionParams,        # the parameters that each thread needs, this can be a single or array of parameter
    	[Parameter(Mandatory=$false)]   
    	[int]$maxCnt=100                # the default amount of powershell threads, if this number is big, then the system might get stuck
    )
	[array]$results=$null # this is used to collect the result from each thread
	$vdiCounts = $vdilist.Count
	If( $vdiCounts -eq 1 ){ # no need to create thread pool to run it
		If( [string]::IsNullOrWhiteSpace($vdilist[0]) ){
		    write-error "vdilist only contains 1 line but it's null"
		}else{$results+=(& $Action $vdilist[0] 1 $ActionParams)}
	}else{
		$vdiCounts = $vdilist.Count ; $throttleLimit=$maxCnt ; If($vdiCounts -le $maxCnt ){$throttleLimit=$vdiCounts}
		# creaate the thread pool and run each thread based the vdi list amount
		$SessionState = [system.management.automation.runspaces.initialsessionstate]::CreateDefault()
		$Pool = [runspacefactory]::CreateRunspacePool(1, $throttleLimit, $SessionState, $Host)
		$Pool.Open() ; $threads = @() ; $cnt=1
		$handles = foreach($line in $vdilist){
			if( [string]::IsNullOrWhiteSpace($line ) ){continue}
			$powershell = [powershell]::Create().AddScript($Action).AddArgument($line).AddArgument($cnt).AddArgument($ActionParams)
			$powershell.RunspacePool = $Pool ;	$powershell.BeginInvoke() ; $threads += $powershell ; $cnt++
		}
		# wait each thread to be done and then collecte its return value
		do{
		  $i = 0 ;  $done = $true
		  foreach ($handle in $handles) {
			if($handle -ne $null){if($handle.IsCompleted){$results+=$threads[$i].EndInvoke($handle);$threads[$i].Dispose();$handles[$i]=$null}else{$done = $false}}
			$i++
		  }
		  if (-not $done) { Start-Sleep -Milliseconds 200 }
		} until ($done)
	}
	return $results # return the value collection from each thread and then export to csv or take other further actions
}
Function script:Check-FIDExistence(){ # this function is used to check if a given FID/group exists in AD
    param(
    	[Parameter(Mandatory=$false)]        
    	[string]$domain="nam",               # the domain that FID belongs to
    	[Parameter(Mandatory=$True)]         
    	[string]$checkvalue,                 # the FID or group name
		[Parameter(Mandatory=$False)]
		[ValidateSet("user", "group")]   
    	[string]$checkType="user"            # check type, can only be user,group, default value is user

    )
	If( $checkvalue -match "\\" ){
		$domain=$checkvalue.split('\')[0]
	    $checkvalue = $checkvalue.split('\')[1] # if check value contains domain name, remove it
	}
	Try{
	    return [ADSI]::Exists("WinNT://$domain/$checkvalue,$checkType")
	}Catch{
	    return $false
	}Finally{}
}
Function script:Export-Result(  ){
	param(
		[Parameter(Mandatory=$True)]
		[object[]]$results,
		[Parameter(Mandatory=$True)]
		[string]$result_type,
		[Parameter(Mandatory=$True)]
		[string]$fileFullname,
		[Parameter(Mandatory=$True)]
		[object]$run_mins,
		[Parameter(Mandatory=$false)]
		[switch]$outfile
	)
	Try{
		If( !$outfile ){
			$results|Export-Csv -Path $fileFullname -UseCulture -NoTypeInformation -Encoding UTF8 -Force -ErrorAction Stop
		}else{
		    $results|out-file -FilePath $fileFullname -Encoding utf8 -Force
		}
		Write-Host -ForegroundColor Yellow "Done export $result_type to $fileFullname in $run_mins s."
	}Catch{
		write-error "Failed to combine csv files" ; $_
	}Finally{}

}
Function script:New-FileOrFolder($p,[Switch]$d){
  if(!(test-path $p -ErrorAction Ignore)){if($d){new-item -ItemType Directory -path $p -Force 1>$null}else{new-item -ItemType File -path $p -Force 1>$null}}
}
Function script:Connect-RDC(){
	param(
		[Parameter(Mandatory=$True)]
		[string]$vdi,
		[string]$fid,
		[string]$passwd
	)
	cmdkey.exe /generic:$vdi /user:$fid /pass:$passwd 1>$null
	Try {
        If ($script:vmserver_list -imatch $vdi) {
            mstsc.exe /v $vdi /w 1920 /h 1080
        } else {
            mstsc.exe /v $vdi /w 1376 /h 768
        }
    } Catch {
        [void][System.Windows.Forms.MessageBox]::Show($_, 'RDC Connecting Error', 'OK', 'Error')
    } Finally { }
}
Function script:Load-UpdateConfigFile(){
    param(
        [Parameter(Mandatory=$False)]
        [string]$user,
        [Parameter(Mandatory=$False)]
        [string]$pass,
        [Parameter(Mandatory=$False)]
        [ValidateSet("load", "update")]
        [string]$config_action="load",
        [Parameter(Mandatory=$False)]
        [string]$load_user='aaadmin'
    )
    $config_xml_file="$this_path\config\config.xml"
    $original_config_xml=@"
<?xml version="1.0" encoding="UTF-8"?>
<Config>
    <login>
    </login>
</Config>
"@
    Switch($config_action){
        "load"{
            If( (test-path $config_xml_file -ea 0)){
                $config_xml=[xml](cat $config_xml_file)
                If( ($user_node_rst=$config_xml.SelectSingleNode("//login/$this_user-$this_machine-$load_user-user")) -ne $null  ){
                    $TextboxFID.Text='nam\'+$user_node_rst.'#text'
                }
                If( ($pass_node_rst=$config_xml.SelectSingleNode("//login/$this_user-$this_machine-$load_user-pass")) -ne $null  ){
                    Try{
                        $plain_pass=[Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR((ConvertTo-SecureString ($pass_node_rst.'#text') -ea 0))) 2>$null 
                    }Catch{}Finally{$TextboxPass.Text=$plain_pass}
                }
            }
            break
        }
        "update"{
            try{$user=$user.split('\')[1] 2>$null}catch{}
            Try{$pass=$pass|ConvertTo-SecureString -AsPlainText -Force|ConvertFrom-SecureString}Catch{}Finally{}
            If( !(test-path $config_xml_file -ea 0)){
                $original_config_xml|out-file -file $config_xml_file -Encoding utf8 -Force
            }
            $config_string=cat $config_xml_file
            If( ($user_node_rst=select-xml -path $config_xml_file -xpath "//login/$this_user-$this_machine-$user-user" ) -eq $null ){
                $config_string=$config_string -replace([regex]::Escape("</login>"),("`t<$this_user-$this_machine-{0}-user>{0}</$this_user-$this_machine-{0}-user>`n`t</login>" -f $user))
            }else{
                $user_node_rst.node.'#text' = $user.psobject.baseobject
                $user_node_rst.node.ownerdocument.save($user_node_rst.path)
            }
            If( ($pass_node_rst=select-xml -path $config_xml_file -xpath "//login/$this_user-$this_machine-$user-pass" ) -eq $null ){
                $config_string=$config_string -replace([regex]::Escape("</login>"),("`t<$this_user-$this_machine-{0}-pass>{1}</$this_user-$this_machine-{0}-pass>`n`t</login>" -f $user,$pass))
            }else{
                $pass_node_rst.node.'#text' = $user.psobject.baseobject
                $pass_node_rst.node.ownerdocument.save($pass_node_rst.path)
            }
            $config_string |Out-File -file $config_xml_file -Encoding utf8
            Try{
                $x = Select-Xml -Path "$this_path\config\db_config.xml" -XPath "//credentials/$user/pwd"
                $x.Node.InnerText = $pass.psobject.baseobject
                $x.Node.OwnerDocument.Save($x.Path)
            }Catch{
                Popup-Message $_ "Update XML Node Value Error"
            }Finally{}
        }
    }
}
Function script:Update-Inventory(){
    param(
		[Parameter(Mandatory=$false)]
		[string]$inventory_url='https://globalconsumer.collaborationtools.consumer.citigroup.net/sites/ISG/ISGRPA/Shared%20Documents/Hardware%20Plans/RPA%20Inventory_v5.xlsb',
		[Parameter(Mandatory=$False)]
		[string]$local_folder,
		[Parameter(Mandatory=$False)]
		[string]$password
	)
    $this_path=Split-Path -Parent $MyInvocation.ScriptName
    $this_user=$env:username
    $this_machine=$env:computername
    If( !$PSBoundParameters.ContainsKey('local_folder') ){
        $local_folder="$this_path\config\Inventory.xlsb"
    }
	If( !$PSBoundParameters.ContainsKey('password') ){
        Try{
            $password=([xml](cat "$this_path\config\config.xml")).SelectSingleNode("//login/$this_user-$this_machine-aaadmin-pass").'#text'
            $password = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR((ConvertTo-SecureString $password -ea 1)))
        }Catch{
            Do{
                $credentials = Get-Credential -UserName 'aaadmin' -Message "Please enter password as it was invalid!"
                If( $credentials -ne $null ){
                    $user=$credentials.GetNetworkCredential().username
                    $password = $credentials.GetNetworkCredential().Password
                }else{return}
            }While( $password -eq $null -or [string]::IsNullOrWhiteSpace($password.trim()))
            Load-UpdateConfigFile "nam\$this_user" $password "update"
        }Finally{}
	}
	$webclient=New-Object System.Net.WebClient
    $webclient.UseDefaultCredentials= New-Object System.Net.NetworkCredential('aaadmin',$password)
    Try{$webclient.DownloadFile($inventory_url,$local_folder)}Catch{ Popup-Message $_ 'Downloading File Error' ; return}Finally{}
    #<---------------------- initilize excel com application
    Try{ # the script will quit if no excel has been installed in the running computer
        $excel = new-object -comobject excel.application ; # $excel.Visible=$true
    }Catch{Popup-Message $_ 'Excel Running Error' ;return}Finally{}
    #<-------------------------------------------------- enable macro running
    $macro_key="HKCU\Software\Microsoft\Office\$($excel.Version)\excel\Security"
    reg delete $macro_key /v AccessVBOM /f 1>$null 2>&1
    reg delete $macro_key /v VBAWarnings /f 1>$null 2>&1
    #<--------------------------------------------------open excel and start to combine csv files
    $excel.DisplayAlerts=$false # disable warning popup when makeing changes
    $excel.workbooks.open("$this_path\config\Inventory.xlsb") 1>$null
    $excel.Run("MergeVDIList")
    #<-------------------------------------------------- completely quit and remove excel com-object
    $excel.quit() ; [System.Runtime.Interopservices.Marshal]::ReleaseComObject($excel) 1>$null ; Remove-Variable excel
    (cat "$this_path\config\inventory.txt").replace('"',"") |out-file "$this_path\config\inventory.txt" -Encoding utf8 -Force
    [void][System.Windows.Forms.MessageBox]::Show("Inventory has been updated in '$this_path\config\inventory.txt'", 'Updating Inventory Done', 'OK', 'Information')
}